package com.sojson.util.encrypt.bothway.imparity.sm2.impl.encrypt.two;

import java.math.BigInteger;
import java.security.SecureRandom;
import java.util.Arrays;

import org.bouncycastle.crypto.DerivationFunction;
import org.bouncycastle.crypto.digests.SHA256Digest;
import org.bouncycastle.crypto.digests.ShortenedDigest;
import org.bouncycastle.crypto.generators.KDF1BytesGenerator;
import org.bouncycastle.crypto.params.ISO18033KDFParameters;
import org.bouncycastle.math.ec.ECCurve;
import org.bouncycastle.math.ec.ECPoint;

import com.sojson.util.StringUtil;
import com.sojson.util.Util;
import com.sojson.util.encrypt.bothway.imparity.sm2.bean.Sm2KeyPair;

import lombok.extern.slf4j.Slf4j;

/**
 * Sms2双向非对称加解密工具类
 *
 * @author liu
 * @date 2020-09-04
 */
@Slf4j
public class Sm2Util2Impl {

    private static Sm2Util2Impl instances;

    private final SecureRandom random = new SecureRandom();

    // /** 测试参数 */
    // /** 素数p */
    // private static final String P =
    // "FFFFFFFE" + "FFFFFFFF" + "FFFFFFFF" + "FFFFFFFF" + "FFFFFFFF" + "00000000" + "FFFFFFFF" + "FFFFFFFF";
    // /** 系数a */
    // private static final String A =
    // "FFFFFFFE" + "FFFFFFFF" + "FFFFFFFF" + "FFFFFFFF" + "FFFFFFFF" + "00000000" + "FFFFFFFF" + "FFFFFFFC";
    // /** 系数b */
    // private static final String B =
    // "28E9FA9E" + "9D9F5E34" + "4D5A9E4B" + "CF6509A7" + "F39789F5" + "15AB8F92" + "DDBCBD41" + "4D940E93";
    // /** 基点G, G=(xg,yg),其介记为n */
    // private static final String N =
    // "FFFFFFFE" + "FFFFFFFF" + "FFFFFFFF" + "FFFFFFFF" + "7203DF6B" + "21C6052B" + "53BBF409" + "39D54123";
    // /** 坐标x */
    // private static final String GX =
    // "32C4AE2C" + "1F198119" + "5F990446" + "6A39C994" + "8FE30BBF" + "F2660BE1" + "715A4589" + "334C74C7";
    // /** 坐标y */
    // private static final String GY =
    // "BC3736A2" + "F4F6779C" + "59BDCEE3" + "6B692153" + "D0A9877C" + "C62A4740" + "02DF32E5" + "2139F0A0";

    /** 正式参数 */
    /** 素数p */
    private static final String P =
        "FFFFFFFE" + "FFFFFFFF" + "FFFFFFFF" + "FFFFFFFF" + "FFFFFFFF" + "00000000" + "FFFFFFFF" + "FFFFFFFF";
    /** 系数a */
    private static final String A =
        "FFFFFFFE" + "FFFFFFFF" + "FFFFFFFF" + "FFFFFFFF" + "FFFFFFFF" + "00000000" + "FFFFFFFF" + "FFFFFFFC";
    /** 系数b */
    private static final String B =
        "28E9FA9E" + "9D9F5E34" + "4D5A9E4B" + "CF6509A7" + "F39789F5" + "15AB8F92" + "DDBCBD41" + "4D940E93";
    /** 基点G, G=(xg,yg),其介记为n */
    private static final String N =
        "FFFFFFFE" + "FFFFFFFF" + "FFFFFFFF" + "FFFFFFFF" + "7203DF6B" + "21C6052B" + "53BBF409" + "39D54123";
    /** 坐标x */
    private static final String GX =
        "32C4AE2C" + "1F198119" + "5F990446" + "6A39C994" + "8FE30BBF" + "F2660BE1" + "715A4589" + "334C74C7";
    /** 坐标y */
    private static final String GY =
        "BC3736A2" + "F4F6779C" + "59BDCEE3" + "6B692153" + "D0A9877C" + "C62A4740" + "02DF32E5" + "2139F0A0";
    public static final String[] ECC_PARAM = {P, A, B, N, GX, GY};
    private ECCurve.Fp curve = new ECCurve.Fp(new BigInteger(ECC_PARAM[0], 16), new BigInteger(ECC_PARAM[1], 16),
        new BigInteger(ECC_PARAM[2], 16), null, null);
    private ECPoint g = curve.createPoint(new BigInteger(ECC_PARAM[4], 16), new BigInteger(ECC_PARAM[5], 16));

    /**
     * 加密
     *
     * @param data      内容
     * @param publicKey 公钥
     * @return byte[] 加密后的字节数组
     * @throws Exception
     */
    public String encrypt(byte[] data, ECPoint publicKey) throws Exception {
        return encryptContentToStr(encryptInit(data, publicKey));
    }

    /**
     * 加密
     *
     * @param data      内容
     * @param publicKey 公钥
     * @return byte[] 加密后的字节数组
     */
    private byte[] encryptInit(byte[] data, ECPoint publicKey) {
        printHexString(data);

        /* 1 产生随机数k，k属于[1, n-1] */
        BigInteger k = random(new BigInteger(ECC_PARAM[3], 16));
        printHexString(k.toByteArray());

        /* 2 计算椭圆曲线点C1 = [k]G = (x1, y1) */
        ECPoint c1 = g.multiply(k);
        byte[] c1Buffer = c1.getEncoded(false);
        printHexString(c1Buffer);

        // 3 计算椭圆曲线点 S = [h]Pb * curve没有指定余因子，h为空

        /// BigInteger h = curve.getCofactor();
        /// printHexString(h.toByteArray());
        /// if (publicKey != null) {
        /// ECPoint result = publicKey.multiply(h);
        /// if (!result.isInfinity()) {
        ///
        /// } else {
        /// log.error("计算椭圆曲线点 S = [h]Pb失败");
        /// return null;
        /// }
        /// }

        /* 4 计算 [k]PB = (x2, y2) */
        ECPoint kpb = publicKey.multiply(k).normalize();

        /* 5 计算 t = KDF(x2||y2, klen) */
        byte[] kpbBytes = kpb.getEncoded(false);
        DerivationFunction kdf = new KDF1BytesGenerator(new ShortenedDigest(new SHA256Digest(), 20));
        byte[] t = new byte[data.length];
        kdf.init(new ISO18033KDFParameters(kpbBytes));
        kdf.generateBytes(t, 0, t.length);

        if (allZero(t)) {
            log.error("all zero");
        }

        /* 6 计算C2=M^t */
        byte[] c2 = new byte[data.length];
        for (int i = 0; i < data.length; i++) {
            c2[i] = (byte)(data[i] ^ t[i]);
        }

        /* 7 计算C3 = Hash(x2 || M || y2) */
        byte[] c3 = calculateHash(kpb.getXCoord().toBigInteger(), data, kpb.getYCoord().toBigInteger());

        /* 8 输出密文 C=C1 || C2 || C3 */
        byte[] encryptResult = new byte[c1Buffer.length + c2.length + c3.length];
        System.arraycopy(c1Buffer, 0, encryptResult, 0, c1Buffer.length);
        System.arraycopy(c2, 0, encryptResult, c1Buffer.length, c2.length);
        System.arraycopy(c3, 0, encryptResult, c1Buffer.length + c2.length, c3.length);

        printHexString(encryptResult);

        return encryptResult;
    }

    /**
     * 解密
     *
     * @param data          内容
     * @param privateKey    私钥
     * @return
     */
    public String decrypt(String data, BigInteger privateKey) {
        return decryptInit(decryptContentToByte(data), privateKey);
    }

    /**
     * 解密
     *
     * @param data          内容
     * @param privateKey    私钥
     * @return
     */
    private String decryptInit(byte[] data, BigInteger privateKey) {
        byte[] c1Byte = new byte[65];
        System.arraycopy(data, 0, c1Byte, 0, c1Byte.length);

        ECPoint c1 = curve.decodePoint(c1Byte).normalize();

        /* 计算[dB]C1 = (x2, y2) */
        ECPoint dbc1 = c1.multiply(privateKey).normalize();

        /* 计算t = KDF(x2 || y2, klen) */
        byte[] dbc1Bytes = dbc1.getEncoded(false);
        DerivationFunction kdf = new KDF1BytesGenerator(new ShortenedDigest(new SHA256Digest(), 20));

        int klen = data.length - 65 - 20;

        byte[] t = new byte[klen];
        kdf.init(new ISO18033KDFParameters(dbc1Bytes));
        kdf.generateBytes(t, 0, t.length);

        if (allZero(t)) {
            log.error("all zero");
        }

        /* 5 计算M'=C2^t */
        byte[] m = new byte[klen];
        for (int i = 0; i < m.length; i++) {
            m[i] = (byte)(data[c1Byte.length + i] ^ t[i]);
        }

        /* 6 计算 u = Hash(x2 || M' || y2) 判断 u == C3是否成立 */
        byte[] c3 = new byte[20];
        System.arraycopy(data, data.length - 20, c3, 0, 20);
        byte[] u = calculateHash(dbc1.getXCoord().toBigInteger(), m, dbc1.getYCoord().toBigInteger());
        if (Arrays.equals(u, c3)) {
            return new String(m);
        } else {
            printHexString(u);
            printHexString(c3);
            log.error("解密验证失败");
        }
        return null;
    }

    private String printHexString(byte[] b) {
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < b.length; i++) {
            String hex = Integer.toHexString(b[i] & 0xFF);
            if (hex.length() == 1) {
                builder.append('0' + hex);
                hex = '0' + hex;
            }
            builder.append(hex);
        }
        return builder.toString();
    }

    private BigInteger random(BigInteger max) {
        BigInteger r = new BigInteger(256, random);
        // int count = 1;
        while (r.compareTo(max) >= 0) {
            r = new BigInteger(128, random);
            // count++;
        }
        return r;
    }

    private boolean allZero(byte[] buffer) {
        for (int i = 0; i < buffer.length; i++) {
            if (buffer[i] != 0) {
                return false;
            }
        }
        return true;
    }

    private byte[] calculateHash(BigInteger x2, byte[] m, BigInteger y2) {
        ShortenedDigest digest = new ShortenedDigest(new SHA256Digest(), 20);
        byte[] buf = x2.toByteArray();
        digest.update(buf, 0, buf.length);
        digest.update(m, 0, m.length);
        buf = y2.toByteArray();
        digest.update(buf, 0, buf.length);

        buf = new byte[20];
        digest.doFinal(buf, 0);
        return buf;
    }

    private boolean between(BigInteger param, BigInteger min, BigInteger max) {
        if (param.compareTo(min) >= 0 && param.compareTo(max) < 0) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * 公钥校验
     * @param publicKey 公钥
     * @return boolean true或false
     */
    private boolean checkPublicKey(ECPoint publicKey) {
        if (!publicKey.isInfinity()) {
            BigInteger x = publicKey.getXCoord().toBigInteger();
            BigInteger y = publicKey.getYCoord().toBigInteger();
            String zero = "0";
            int radix = 16;
            int three = 3;
            if (between(x, new BigInteger(zero), new BigInteger(ECC_PARAM[0], radix))
                && between(y, new BigInteger(zero), new BigInteger(ECC_PARAM[0], radix))) {
                BigInteger xResult = x.pow(3).add(new BigInteger(ECC_PARAM[1], radix).multiply(x))
                    .add(new BigInteger(ECC_PARAM[2], 16)).mod(new BigInteger(ECC_PARAM[0], radix));
                BigInteger yResult = y.pow(2).mod(new BigInteger(ECC_PARAM[0], radix));
                if (yResult.equals(xResult)
                    && publicKey.multiply(new BigInteger(ECC_PARAM[three], radix)).isInfinity()) {
                    return true;
                }
            }
            return false;
        } else {
            return false;
        }
    }

    /**
     * 加密
     *
     * @param data      内容
     * @param publicKey 公钥
     * @return
     * @throws Exception
     */
    private String encryptContentToStr(byte[] data) {
        return Util.byteToHex(data);
    }

    /**
     * 解密
     *
     * @param data          内容
     * @param privateKey    私钥
     * @return
     * @throws Exception
     */
    private byte[] decryptContentToByte(String data) {
        return Util.hexToByte(data);
    }

    /**
     * 获得公私钥对
     * @return
     * @throws Exception
     */
    public Sm2KeyPair generateKeyPair() throws Exception {
        BigInteger d = random(new BigInteger(ECC_PARAM[3], 16).subtract(new BigInteger("1")));
        Sm2KeyPair keyPair = new Sm2KeyPair(g.multiply(d).normalize(), d);
        if (checkPublicKey(keyPair.getPublicKeyPoint())) {
            return keyPair;
        } else {
            log.error("generate key failed");
            return null;
        }
    }

    public static void main(String[] args) throws Exception {
        String m =
            "哈哈哈，&*&…………&、、//\\!@#$%^&*()物品woyebuzhidaowozijiqiaodesha!@#$%^&*())))))ooooooooppppppppppppppppppplllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkffffffffffffffffffffffffffffffffffffff";
        byte[] bytes = m.getBytes();
        Sm2KeyPair keyPair = null;
        String publicKey = null;
        String privateKey = null;
        Sm2Util2Impl instances = Sm2Util2Impl.getInstances();

        keyPair = instances.generateKeyPair();
        publicKey = keyPair.getPublicKeyStr();
        privateKey = keyPair.getPrivateKeyStr();
        System.out.println("公钥 : " + publicKey);
        System.out.println("私钥 : " + privateKey);
        System.out.println("================================================");
        System.out.println("================================================");
        System.out.println("================================================");

        String encrypt2 = null;
        String decrypt2 = null;

        ECPoint publicKeyObj = keyPair.getPublicKeyPoint();
        BigInteger privateKeyObj = keyPair.getPrivateKeyInt();
        String data = instances.encrypt(bytes, publicKeyObj);
        System.out.println("加密: " + new String(data));
        String decrypt = instances.decrypt(data, privateKeyObj);
        System.out.println("解密: " + decrypt);

        System.out.println("================================================");
        System.out.println("================================================");
        System.out.println("================================================");

        long currentTimeMillis = 0L;
        long currentTimeMillis2 = 0L;
        int size = 100;

        currentTimeMillis = System.currentTimeMillis();
        for (int i = 0; i < size; i++) {
            encrypt2 = instances.encrypt(bytes, publicKeyObj);
            decrypt2 = instances.decrypt(encrypt2, privateKeyObj);
            if (!m.equals(decrypt2)) {
                System.out.println(decrypt2);
            }
        }
        currentTimeMillis2 = System.currentTimeMillis();
        System.out.println(currentTimeMillis2 - currentTimeMillis);

        System.out.println("================================================");
        System.out.println("================================================");
        System.out.println("================================================");
    }

    /**
     * 判断一个对象是否是空(空对象)
     *
     * @param obj
     * @return
     */
    private static boolean isBlankObject(Object obj) {
        return StringUtil.isBlankObject(obj);
    }

    public static Sm2Util2Impl getInstances() {
        if (isBlankObject(instances)) {
            instances = new Sm2Util2Impl();
        }
        return instances;
    }

}